﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Reflection;
using System.Threading;
using System.IO;
using System.Net;
using System.Drawing.Imaging;

namespace Map
{
    #region TiledMap
    public interface IMapRenderer
    {
        IMapDrawable GetBitmapFromStream(TileClient session, Stream stream);
        void Draw(IMapDrawable drawable, Rectangle destRect, Rectangle sourceRect);
        void FillRectangle(Color color, Rectangle rect);
        void DrawLines(float lineWidth, Color color, Point[] points);
    }

    public interface IMapDrawable : IDisposable
    {
        int Width { get; }
        int Height { get; }
    }

    public class TileData
    {
        public int LastUsed { get; set; }
        public IMapDrawable Bitmap { get; set; }

        public TileData() { }

        public TileData(IMapDrawable bitmap)
        {
            Bitmap = bitmap;
        }
    }

    public abstract class TileClient : IDisposable
    {
        public IMapDrawable RefreshBitmap { get; set; }
        public abstract int MaxZoom { get; }

        const double EARTH_RADIUS = 6378137;
        const double EARTH_CIRCUM = EARTH_RADIUS * 2.0 * Math.PI;
        const double EARTH_HALF_CIRC = EARTH_CIRCUM / 2;
        const int OFFSET = 268435456;
        const double MAP_RADIUS = OFFSET / Math.PI;

        /// <summary>
        /// Converts longitude to x coordinate
        /// </summary>
        /// <param name="lon">Decimal longitude</param>
        /// <returns>X coordinate</returns>
        public static int LonToX(double lon)
        {
            return (int)Math.Round(OFFSET + MAP_RADIUS * lon * Math.PI / 180);
        }
        /// <summary>
        /// Converts latitude to y coordinate
        /// </summary>
        /// <param name="lat">Decimal latitude</param>
        /// <returns>Y coordinate</returns>
        public static int LatToY(double lat)
        {
            return (int)Math.Round(OFFSET - MAP_RADIUS * Math.Log((1 + Math.Sin(lat * Math.PI / 180)) / (1 - Math.Sin(lat * Math.PI / 180))) / 2);
        }
        /// <summary>
        /// Converts x coordinate to longitude
        /// </summary>
        /// <param name="x">X coordinate</param>
        /// <returns>Decimal longitude</returns>
        public static double XToLon(int x)
        {
            return ((x - OFFSET) / MAP_RADIUS) * 180 / Math.PI;
        }
        /// <summary>
        /// Converts y coordinate to latitude
        /// </summary>
        /// <param name="y">Y coordinate</param>
        /// <returns>Decimal latitude</returns>
        public static double YToLat(int y)
        {
            return (Math.PI / 2 - 2 * Math.Atan(Math.Exp((y - OFFSET) / MAP_RADIUS))) * 180 / Math.PI;
        }

        static double YToLatitudeAtZoom(int y, int zoom)
        {
            double arc = EARTH_CIRCUM / (1 << zoom);
            double metersY = EARTH_HALF_CIRC - (y * arc);
            double a = Math.Exp(metersY * 2 / EARTH_RADIUS);
            double result = RadToDeg(Math.Asin((a - 1) / (a + 1)));
            return result;
        }

        static double XToLongitudeAtZoom(int x, int zoom)
        {
            double arc = EARTH_CIRCUM / (1 << zoom);
            double metersX = (x * arc) - EARTH_HALF_CIRC;
            double result = RadToDeg(metersX / EARTH_RADIUS);
            return result;
        }

        static int LatitudeToYAtZoom(double lat, int zoom)
        {
            double arc = EARTH_CIRCUM / (1 << zoom);
            double result = DegToRad(lat);
            result = Math.Sin(result);
            result = (result + 1) / (1 - result);
            result = Math.Log(result);
            result *= EARTH_RADIUS / 2;
            result = (EARTH_HALF_CIRC - result) / arc;
            return (int)Math.Round(result);
        }

        static int LongitudeToXAtZoom(double lon, int zoom)
        {
            double arc = EARTH_CIRCUM / (1 << zoom);
            double result = DegToRad(lon);
            result *= EARTH_RADIUS;
            result += EARTH_HALF_CIRC;
            result /= arc;
            return (int)Math.Round(result);
        }

        public GeoPoint CenterToGeoPoint()
        {
            GeoPoint ret = new GeoPoint();
            ret.Longitude = XToLongitudeAtZoom((centerTile.X << 8) + centerOffset.X, zoom + 8);
            ret.Latitude = YToLatitudeAtZoom((centerTile.Y << 8) + centerOffset.Y, zoom + 8);
            return ret;
        }

        public GeoPoint ScreenToGeoPoint(Point point, Size clientSize)
        {
            int DX = ((point.X - clientSize.Width / 2) << (21 - (int)zoom));
            int DY = ((point.Y - clientSize.Height / 2) << (21 - (int)zoom));
            GeoPoint centerGeoPoint = CenterToGeoPoint();
            int OX = LonToX(centerGeoPoint.Longitude) + DX;
            double longitude = XToLon(OX);
            double latitude = YToLat(LatToY(centerGeoPoint.Latitude) + DY);
            return new GeoPoint(latitude, longitude);
        }

        public Point GeoPointToCenterRelativePoint(GeoPoint geocode)
        {
            int centerXReference = centerTile.X << 8 + centerOffset.X;
            int centerYReference = centerTile.Y << 8 + centerOffset.Y;
            int px = LongitudeToXAtZoom(geocode.Longitude, zoom + 8);
            int py = LatitudeToYAtZoom(geocode.Latitude, zoom + 8);
            return new Point(px - centerXReference, py - centerYReference);
        }

        static double DegToRad(double rad)
        {
            return rad * Math.PI / 180.0;
        }

        static double RadToDeg(double d)
        {
            return d / Math.PI * 180.0;
        }

        static GeoPoint PointToGeocode(Point point, int zoom)
        {
            return new GeoPoint(YToLatitudeAtZoom(point.Y, zoom), XToLongitudeAtZoom(point.X, zoom));
        }

        public struct Key
        {
            public static readonly Key Root = new Key();

            public int X;
            public int Y;
            public int Zoom;
            public Key(int x, int y, int zoom)
            {
                X = x;
                Y = y;
                Zoom = zoom;
            }

            public bool IsValid
            {
                get
                {
                    if (X < 0 || Y < 0 || X >= 1 << Zoom || Y >= 1 << Zoom)
                        return false;
                    return true;
                }
            }

            public override int GetHashCode()
            {
                return X ^ Y ^ (Zoom << 24);
            }

            public static implicit operator Point(Key key)
            {
                return new Point(key.X, key.Y);
            }

            public static bool operator ==(Key first, Key second)
            {
                return first.X == second.X && first.Y == second.Y && first.Zoom == second.Zoom;
            }

            public static bool operator !=(Key first, Key second)
            {
                return first.X != second.X || first.Y != second.Y || first.Zoom != second.Zoom;
            }

            public override bool Equals(object obj)
            {
                Key key = (Key)obj;
                return key == this;
            }
        }

        public virtual bool HasAlpha
        {
            get { return false; }
        }

        public TileClient()
        {
            BackColor = Color.Gray;
        }

        public Color BackColor { get; set; }

        public abstract TileData GetTile(TileClient.Key key, IMapRenderer renderer, System.Threading.WaitCallback callback, object state);
        static readonly SolidBrush grayBrush = new SolidBrush(Color.Gray);

        static bool GeocodeBoxContains(GeoPoint tlGeo, GeoPoint brGeo, GeoPoint geocode)
        {
            return geocode.Latitude > brGeo.Latitude && geocode.Latitude < tlGeo.Latitude && geocode.Longitude > tlGeo.Longitude && geocode.Longitude < brGeo.Longitude;
        }

        static readonly Pen directionsPen = new Pen(Color.FromArgb(unchecked((int)0xC049BBE5)), 6);

        public int DrawMap(IMapRenderer renderer, int x, int y, int width, int height, WaitCallback callback, object state)
        {
            int unavailable = 0;

            // approximate the the top left tile (it may be off by 1), but the loop
            // below will kept it from being drawn
            int midX = x + width / 2 - centerOffset.X;
            int midY = y + height / 2 - centerOffset.Y;
            int xTiles = (midX - x) / 256 + 1;
            int yTiles = (midY - y) / 256 + 1;
            Key currentXKey = new Key(centerTile.X - xTiles, centerTile.Y - yTiles, zoom);
            int xStart = midX - xTiles * 256;
            int yStart = midY - yTiles * 256;
            Rectangle rect = new Rectangle(x, y, width, height);

            int tickCount = Environment.TickCount;
            for (int currentX = xStart; currentX < x + width; currentX += 256, currentXKey.X++)
            {
                Key key = currentXKey;
                for (int currentY = yStart; currentY < y + height; currentY += 256, key.Y++)
                {
                    IMapDrawable tile = null;

                    // find the intersect region of the tile that we are drawing
                    Rectangle tileRect = new Rectangle(currentX, currentY, 256, 256);
                    tileRect.Intersect(rect);
                    Rectangle sourceRect = new Rectangle(tileRect.X - currentX, tileRect.Y - currentY, tileRect.Width, tileRect.Height);

                    // dont draw off the map tiles
                    if (!key.IsValid)
                    {
                        // dont draw gray rect if we're drawing transparent
                        if (!HasAlpha)
                            renderer.FillRectangle(BackColor, tileRect);
                        continue;
                    }

                    // first try to get the tile from the tileData 
                    TileData tileData = GetTile(key, renderer, callback, state);

                    if (tileData != null)
                    {
                        tile = tileData.Bitmap;
                        tileData.LastUsed = tickCount;
                    }

                    if (tile == null)
                    {
                        // tile not available, so try to generate a tile from child tiles
                        unavailable++;

                        Key childKey = new Key(key.X * 2, key.Y * 2, key.Zoom + 1);
                        Key tl = childKey;
                        Key tr = new Key(childKey.X + 1, childKey.Y, childKey.Zoom);
                        Key br = new Key(childKey.X + 1, childKey.Y + 1, childKey.Zoom);
                        Key bl = new Key(childKey.X, childKey.Y + 1, childKey.Zoom);
                        TileData tld;
                        TileData trd;
                        TileData bld;
                        TileData brd;

                        // see if the children are available
                        // we also need to null check, because they could be loading
                        if (TileCache.TryGetValue(tl, out tld) && TileCache.TryGetValue(tr, out trd) && TileCache.TryGetValue(br, out brd) && TileCache.TryGetValue(bl, out bld)
                            && tld != null && trd != null && bld != null & brd != null
                            && tld.Bitmap != null && trd.Bitmap != null && bld.Bitmap != null && brd.Bitmap != null)
                        {
                            // children are available, so mark them as recently used
                            tld.LastUsed = trd.LastUsed = bld.LastUsed = brd.LastUsed = tickCount;

                            // calculate the destination rects of each child tile
                            Rectangle tlr = new Rectangle(currentX, currentY, 128, 128);
                            Rectangle trr = new Rectangle(currentX + 128, currentY, 128, 128);
                            Rectangle blr = new Rectangle(currentX, currentY + 128, 128, 128);
                            Rectangle brr = new Rectangle(currentX + 128, currentY + 128, 128, 128);

                            tlr.Intersect(rect);
                            trr.Intersect(rect);
                            blr.Intersect(rect);
                            brr.Intersect(rect);

                            // calculate the source rect of each child tile
                            Rectangle tlsr = new Rectangle(tlr.X - currentX, tlr.Y - currentY, tlr.Width * 2, tlr.Height * 2);
                            Rectangle trsr = new Rectangle(trr.X - currentX - 128, trr.Y - currentY, trr.Width * 2, trr.Height * 2);
                            Rectangle blsr = new Rectangle(blr.X - currentX, blr.Y - currentY - 128, blr.Width * 2, blr.Height * 2);
                            Rectangle brsr = new Rectangle(brr.X - currentX - 128, brr.Y - currentY - 128, brr.Width * 2, brr.Height * 2);

                            // don't attempt to draw tiles that we don't need to
                            if (tlsr.Width > 0 && tlsr.Height > 0)
                                renderer.Draw(tld.Bitmap, tlr, tlsr);
                            if (trsr.Width > 0 && trsr.Height > 0)
                                renderer.Draw(trd.Bitmap, trr, trsr);
                            if (blsr.Width > 0 && blsr.Height > 0)
                                renderer.Draw(bld.Bitmap, blr, blsr);
                            if (brsr.Width > 0 && brsr.Height > 0)
                                renderer.Draw(brd.Bitmap, brr, brsr);
                            continue;
                        }
                        else
                        {
                            // can't generate from children, so try generating one of the parents
                            Key parent = key;
                            Rectangle parentRect = sourceRect;
                            TileData parentData = null;
                            while (parent.Zoom >= 0 && parentData == null)
                            {
                                parentRect.Width /= 2;
                                parentRect.Height /= 2;
                                parentRect.X /= 2;
                                parentRect.Y /= 2;
                                if (parent.X % 2 == 1)
                                    parentRect.X += 128;
                                if (parent.Y % 2 == 1)
                                    parentRect.Y += 128;
                                parent.X /= 2;
                                parent.Y /= 2;
                                parent.Zoom--;
                                TileCache.TryGetValue(parent, out parentData);
                            }

                            if (parentData != null && parentData.Bitmap != null)
                            {
                                // mark this tile as used recently
                                parentData.LastUsed = tickCount;
                                if (tileRect.Width > 0 && tileRect.Height > 0)
                                    renderer.Draw(parentData.Bitmap, tileRect, parentRect);
                                continue;
                            }
                            else
                            {
                                // tile is being downloaded, and we have no parent or child images we can use to draw a temp
                                // image. let's try to use a refresh bitmap.

                                // tile is not available, and this is a transparent draw,
                                // so dont draw at all
                                if (HasAlpha)
                                    continue;
                                if ((tile = RefreshBitmap) == null)
                                {
                                    renderer.FillRectangle(BackColor, tileRect);
                                    continue;
                                }
                            }
                        }
                    }

                    if (tile != null && tileRect.Width > 0 && tileRect.Height > 0)
                        renderer.Draw(tile, tileRect, sourceRect);
                }
            }

            int pixelLevelZoom = zoom + 8;
            int centerXReference = centerTile.X << 8;
            int centerYReference = centerTile.Y << 8;
            GeoPoint tlGeo = PointToGeocode(new Point(Math.Max(centerXReference + centerOffset.X - width / 2, 0), Math.Max(centerYReference + centerOffset.Y - height / 2, 0)), pixelLevelZoom);
            GeoPoint brGeo = PointToGeocode(new Point(Math.Min(centerXReference + centerOffset.X + width / 2, 1 << pixelLevelZoom), Math.Min(centerYReference + centerOffset.Y + height / 2, 1 << pixelLevelZoom)), pixelLevelZoom);
            int adjustX = midX - centerXReference;
            int adjustY = midY - centerYReference;

            foreach (Route route in routes)
            {
                List<Point> points = new List<Point>();
                GeoPoint lastOffscreenPoint = GeoPoint.Null;
                for (int i = 0; i < route.PolyLine.Length; i++)
                {
                    GeoPoint geocode = route.PolyLine[i];
                    if (levelToZoom[route.Levels[i]] > zoom)
                        continue;

                    // check if we're drawing off the screen
                    if (!GeocodeBoxContains(tlGeo, brGeo, geocode))
                    {
                        // if we're drawing from on screen to off screen, draw it, but note that
                        // we are now off screen
                        if (lastOffscreenPoint == GeoPoint.Null)
                            points.Add(GeocodeToScreen(geocode, pixelLevelZoom, adjustX, adjustY));

                        lastOffscreenPoint = geocode;
                        continue;
                    }

                    // draw in from off the screen if necessary
                    if (lastOffscreenPoint != GeoPoint.Null)
                        points.Add(GeocodeToScreen(lastOffscreenPoint, pixelLevelZoom, adjustX, adjustY));
                    // note that we are now in screen space
                    lastOffscreenPoint = GeoPoint.Null;

                    points.Add(GeocodeToScreen(geocode, pixelLevelZoom, adjustX, adjustY));
                }
                if (points.Count > 1)
                    renderer.DrawLines(route.LineWidth, Color.Cyan, points.ToArray());
            }

            foreach (IMapOverlay overlay in Overlays)
            {
                foreach (IMapDrawable drawable in overlay.DrawableItems)
                {
                    DrawAtGeocode(tlGeo, brGeo, renderer, overlay.Point, pixelLevelZoom, adjustX + overlay.Offset.X, adjustY + overlay.Offset.Y, drawable);
                }
            }

            return unavailable;
        }

        void DrawAtGeocode(GeoPoint tlGeo, GeoPoint brGeo, IMapRenderer renderer, GeoPoint geocode, int pixelLevelZoom, int adjustX, int adjustY, IMapDrawable drawable)
        {
            if (!GeocodeBoxContains(tlGeo, brGeo, geocode) || drawable == null)
                return;
            Point p = GeocodeToScreen(geocode, pixelLevelZoom, adjustX, adjustY);
            renderer.Draw(drawable, new Rectangle(p.X - drawable.Width / 2, p.Y - drawable.Height / 2, drawable.Width, drawable.Height), new Rectangle(0, 0, drawable.Width, drawable.Height));
        }

        Point GeocodeToScreen(GeoPoint geocode, int zoom, int adjustX, int adjustY)
        {
            Point p = new Point(LongitudeToXAtZoom(geocode.Longitude, zoom), LatitudeToYAtZoom(geocode.Latitude, zoom));
            p.X += adjustX;
            p.Y += adjustY;
            return p;
        }

        static readonly int[] levelToZoom = new int[] { 13, 7, 2, Int32.MinValue };

        public void Pan(int x, int y)
        {
            int newX = centerOffset.X - x;
            int newY = centerOffset.Y - y;
            Key newCenterTile = new Key(centerTile.X, centerTile.Y, zoom);

            while (newX < 0)
            {
                newX += 256;
                newCenterTile.X--;
            }
            while (newX > 256)
            {
                newX -= 256;
                newCenterTile.X++;
            }

            while (newY < 0)
            {
                newY += 256;
                newCenterTile.Y--;
            }
            while (newY > 256)
            {
                newY -= 256;
                newCenterTile.Y++;
            }

            if (!newCenterTile.IsValid)
                return;

            centerTile = newCenterTile;
            centerOffset = new Point(newX, newY);
        }

        public bool CanZoomIn
        {
            get
            {
                return zoom < this.MaxZoom;
            }
        }

        public bool CanZoomOut
        {
            get
            {
                return zoom > 0;
            }
        }

        public void ZoomIn()
        {
            if (!CanZoomIn)
                return;
            Point newCenterTile = new Point(centerTile.X * 2, centerTile.Y * 2);
            if (centerOffset.X >= 128)
                newCenterTile.X++;
            if (centerOffset.Y >= 128)
                newCenterTile.Y++;
            centerTile = newCenterTile;
            centerOffset.X = (centerOffset.X % 128) * 2;
            centerOffset.Y = (centerOffset.Y % 128) * 2;
            zoom++;
        }


        public void ZoomOut()
        {
            if (zoom <= 0)
                return;

            bool left = centerTile.X % 2 == 0;
            bool top = centerTile.Y % 2 == 0;
            zoom--;

            centerTile = new Point(centerTile.X / 2, centerTile.Y / 2);

            centerOffset.X = centerOffset.X / 2;
            centerOffset.Y = centerOffset.Y / 2;
            if (!left)
                centerOffset.X += 128;
            if (!top)
                centerOffset.Y += 128;
        }


        public void FitPOIToDimensions(int width, int height, int maxZoom, params GeoPoint[] geocodes)
        {
            // find the center
            GeoPoint topLeft = new GeoPoint(double.NegativeInfinity, double.PositiveInfinity);
            GeoPoint bottomRight = new GeoPoint(double.PositiveInfinity, double.NegativeInfinity);
            foreach (GeoPoint geocode in geocodes)
            {
                topLeft.Latitude = Math.Max(topLeft.Latitude, geocode.Latitude);
                topLeft.Longitude = Math.Min(topLeft.Longitude, geocode.Longitude);
                bottomRight.Latitude = Math.Min(bottomRight.Latitude, geocode.Latitude);
                bottomRight.Longitude = Math.Max(bottomRight.Longitude, geocode.Longitude);
            }
            GeoPoint center = new GeoPoint((topLeft.Latitude + bottomRight.Latitude) / 2, (topLeft.Longitude + bottomRight.Longitude) / 2);

            // center the map on the center
            zoom = maxZoom;
            int y = LatitudeToYAtZoom(center.Latitude, zoom + 8);
            int x = LongitudeToXAtZoom(center.Longitude, zoom + 8);
            centerTile.X = x / 256;
            centerTile.Y = y / 256;
            centerOffset.X = x % 256;
            centerOffset.Y = y % 256;

            int halfWidth = width / 2;
            int halfHeight = height / 2;

            while (zoom > 0)
            {
                ZoomOut();
                y = LatitudeToYAtZoom(center.Latitude, zoom + 8);
                x = LongitudeToXAtZoom(center.Longitude, zoom + 8);
                int tly = LatitudeToYAtZoom(topLeft.Latitude, zoom + 8);
                int tlx = LongitudeToXAtZoom(topLeft.Longitude, zoom + 8);
                int bry = LatitudeToYAtZoom(bottomRight.Latitude, zoom + 8);
                int brx = LongitudeToXAtZoom(bottomRight.Longitude, zoom + 8);
                if (tlx - x > -halfWidth && tly - y > -halfHeight && brx - x < halfWidth && bry - y < halfHeight)
                    break;
            }
        }

        Dictionary<Key, TileData> tileCache = new Dictionary<Key, TileData>();

        public Dictionary<Key, TileData> TileCache
        {
            get { return tileCache; }
            set { tileCache = value; }
        }

        public object SynchronizationObject
        {
            get
            {
                return this;
            }
        }

        public virtual void ClearTileCache()
        {
            if (TileCache == null)
                return;
            foreach (TileData data in TileCache.Values)
            {
                if (data != null && data.Bitmap != null)
                    data.Bitmap.Dispose();
            }
            TileCache.Clear();
        }

        public void ClearAgedTiles()
        {
            int cutoff = Environment.TickCount - Map.Settings.CacheInterval;

            List<Key> keys = new List<Key>(TileCache.Keys);
            foreach (Key key in keys)
            {
                TileData data;
                if (!TileCache.TryGetValue(key, out data) || data == null || data.Bitmap == null)
                    continue;

                if (data.LastUsed <= cutoff)
                {
                    data.Bitmap.Dispose();
                    TileCache.Remove(key);
                }
            }
        }

        public void ClearOlderTiles(int maxTiles)
        {
            List<KeyValuePair<Key, TileData>> pairs = new List<KeyValuePair<Key, TileData>>();
            foreach (Key key in TileCache.Keys)
            {
                TileData data = TileCache[key];

            }
        }

        Point centerTile = Point.Empty;
        public Point CenterTile
        {
            get { return centerTile; }
            set { centerTile = value; }
        }

        int zoom = 0;
        public int Zoom
        {
            get { return zoom; }
            set
            {
                zoom = Math.Max(Math.Min(15, value), 0);
            }
        }

        Point centerOffset = new Point(128, 128);
        public Point CenterOffset
        {
            get { return centerOffset; }
            set { centerOffset = value; }
        }

        List<Route> routes = new List<Route>();
        public List<Route> Routes
        {
            get
            {
                return routes;
            }
        }

        List<IMapOverlay> overlays = new List<IMapOverlay>();

        public List<IMapOverlay> Overlays
        {
            get { return overlays; }
        }

        #region IDisposable Members

        public void Dispose()
        {
            ClearTileCache();
            TileCache = null;
            overlays = null;
            routes = null;
        }

        #endregion
    }

    public delegate IMapDrawable GetTileBitmapHandler(TileClient session, Stream stream);

    #endregion

    #region HttpTiledMap
    public abstract class HttpClient : TileClient
    {
        protected abstract Uri GetUriForKey(Key key);

        string cachePath = string.Empty;
        public HttpClient()
        {
            cachePath = Path.Combine(Settings.CachePath, GetType().Name);
            try
            {
                Directory.CreateDirectory(cachePath);
            }
            catch (Exception)
            {
            }
        }

        string GetTilePathForKey(Key key)
        {
            return Path.Combine(cachePath, string.Format("{0}-{1}-{2}", key.X, key.Y, key.Zoom));
        }

        public override TileData GetTile(TileClient.Key key, IMapRenderer renderer, System.Threading.WaitCallback callback, object state)
        {
            TileData tileData = null;
            // try to get the tile
            if (TileCache.TryGetValue(key, out tileData))
                return tileData;

            // check if it is in the file cache
            string tilePath = GetTilePathForKey(key);
            if (File.Exists(tilePath))
            {
                FileInfo finfo = new FileInfo(tilePath);
                if ((DateTime.Now - finfo.CreationTime > Map.Settings.PersistentInterval) &&
                    !Settings.IsPreservable(key))
                {
                    // tile is old, expire it
                    File.Delete(tilePath);
                }
                else
                {
                    try
                    {
                        using (FileStream fstream = new FileStream(tilePath, FileMode.Open))
                        {
                            IMapDrawable bitmap = renderer.GetBitmapFromStream(this, fstream);
                            return TileCache[key] = new TileData(bitmap);
                        }
                    }
                    catch (Exception)
                    {
                        File.Delete(tilePath);
                    }
                }
            }

            // check if its a bad key
            Uri uri = GetUriForKey(key);
            if (uri != null)
            {
                // mark tile as being downloaded
                TileCache[key] = null;
                GetTileData data = new GetTileData();
                data.Renderer = renderer;
                data.Key = key;
                data.Callback = callback;
                data.State = state;
                data.Uri = uri;
                //ThreadPool.QueueUserWorkItem(new WaitCallback(GetTile), data);
                GetTile(data);
            }
            return tileData;
        }

        void CleanupTileData(GetTileData data)
        {
            using (data)
            {
                if (TileCache != null)
                {
                    TileCache.Remove(data.Key);
                }
            }
        }

        void ReadCallback(IAsyncResult result)
        {
            GetTileData data = (GetTileData)result.AsyncState;

            try
            {
                int read = data.ResponseStream.EndRead(result);
                if (read > 0)
                {
                    data.MemoryStream.Write(data.Buffer, 0, read);
                    data.ResponseStream.BeginRead(data.Buffer, 0, data.Buffer.Length, new AsyncCallback(ReadCallback), data);
                }
                else
                {
                    using (data)
                    {
                        string tilePath = GetTilePathForKey(data.Key);
                        using (FileStream file = new FileStream(tilePath, FileMode.Create, FileAccess.Write))
                        {
                            file.Write(data.MemoryStream.GetBuffer(), 0, (int)data.MemoryStream.Length);
                        }

                        data.MemoryStream.Seek(0, SeekOrigin.Begin);
                        IMapDrawable pbitmap = data.Renderer.GetBitmapFromStream(this, data.MemoryStream);
                        if (pbitmap == null)
                            throw new Exception();
                        TileCache[data.Key] = new TileData(pbitmap);
                        data.Callback(data.State);
                    }
                }
            }
            catch (Exception)
            {
                CleanupTileData(data);
            }
        }

        void GetResponseCallback(IAsyncResult result)
        {
            GetTileData data = (GetTileData)result.AsyncState;
            try
            {
                data.Response = data.Request.EndGetResponse(result) as HttpWebResponse;
                data.MemoryStream = new MemoryStream();
                data.ResponseStream = data.Response.GetResponseStream();
                data.Buffer = new byte[1 << 16];

                data.ResponseStream.BeginRead(data.Buffer, 0, data.Buffer.Length, new AsyncCallback(ReadCallback), data);
            }
            catch (Exception)
            {
                CleanupTileData(data);
            }
        }

        void GetTile(GetTileData data)
        {
            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(GetUriForKey(data.Key));
                request.Timeout = 15000;
                request.Method = "GET";
                request.UserAgent = "Windows-RSS-Platform/1.0 (MSIE 7.0; Windows NT 5.1)";
                data.Request = request;
                request.BeginGetResponse(new AsyncCallback(GetResponseCallback), data);
            }
            catch (Exception)
            {
                CleanupTileData(data);
            }
        }

        struct GetTileData : IDisposable
        {
            public WaitCallback Callback;
            public object State;
            public HttpWebRequest Request;
            public HttpWebResponse Response;
            public MemoryStream MemoryStream;
            public Stream ResponseStream;
            public byte[] Buffer;
            public Key Key;
            public Uri Uri;
            public IMapRenderer Renderer;

            #region IDisposable Members

            public void Dispose()
            {
                using (Response)
                {
                    Response = null;
                }
                using (ResponseStream)
                {
                    ResponseStream = null;
                }
                using (MemoryStream)
                {
                    MemoryStream = null;
                }
                Request = null;
                Buffer = null;
                Callback = null;
                State = null;
            }

            #endregion
        }

        void GetTile(object o)
        {
            using (GetTileData data = (GetTileData)o)
            {
                try
                {
                    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(data.Uri);
                    request.Timeout = 15000;
                    request.Method = "GET";
                    request.UserAgent = "Windows-RSS-Platform/1.0 (MSIE 7.0; Windows NT 5.1)";

                    using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
                    {
                        if (response.StatusCode != HttpStatusCode.OK)
                            throw new Exception("Error while downloading tile.");

                        if (TileCache != null)
                        {
                            using (Stream s = response.GetResponseStream())
                            {
                                using (MemoryStream mem = new MemoryStream())
                                {
                                    int read = 0;
                                    byte[] buffer = new byte[10000];
                                    do
                                    {
                                        read = s.Read(buffer, 0, buffer.Length);
                                        mem.Write(buffer, 0, read);
                                    }
                                    while (read != 0);
                                    mem.Seek(0, SeekOrigin.Begin);

                                    string tilePath = GetTilePathForKey(data.Key);
                                    using (FileStream file = new FileStream(tilePath, FileMode.Create, FileAccess.Write))
                                    {
                                        file.Write(mem.GetBuffer(), 0, (int)mem.Length);
                                    }

                                    IMapDrawable pbitmap = data.Renderer.GetBitmapFromStream(this, mem);
                                    if (pbitmap == null)
                                        throw new Exception();
                                    TileCache[data.Key] = new TileData(pbitmap);
                                }
                            }
                            if (data.Callback != null)
                                data.Callback(data.State);
                        }
                    }
                }
                catch (Exception)
                {
                    if (TileCache != null)
                    {
                        TileCache.Remove(data.Key);
                    }
                }
            }
        }
    }

    public class GoogleClient : HttpClient
    {
        static int currentTileServer = 0;
        public override int MaxZoom
        {
            get
            {
                return 15;
            }
        }
        protected override Uri GetUriForKey(Key key)
        {
            return new Uri(string.Format("http://mt{0}.google.com/vt/x={1}&y={2}&z={3}", (currentTileServer++) % 4, key.X, key.Y, key.Zoom));
        }
    }

    public class OSMClient : HttpClient
    {
        public override int MaxZoom
        {
            get
            {
                return 17;
            }
        }
        protected override Uri GetUriForKey(Key key)
        {
            return new Uri(string.Format("http://tile.openstreetmap.org/{2}/{0}/{1}.png", key.X, key.Y, key.Zoom));
        }        
    }
    #endregion
}